﻿// ------------------------------------------------------------------------------------
// File:		OperationTracker.Context.cs
// Created:	01/03/2012 11:49 AM
// 
// Last modified:	tri @ 01/03/2012 12:10 PM
// ------------------------------------------------------------------------------------

namespace EntityFramework.OT
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Data;
    using System.Data.Objects;
    using System.Linq;

    public static class OperationTrackerContextExtensions
    {
        #region Public Methods

        public static IObjectWithOperationTracker ApplyChanges(this ObjectContext context, ObjectOperationTracker tracker, bool applyChanges = true)
        {
            var helper = new OperationHelper(context, tracker);
            foreach (var operation in helper.Operations.Where(o => o.State != ObjectState.Unchanged))
            {
                var entity = helper[operation];
                if (entity == null)
                {
                    throw new InvalidOperationException(string.Format("Could not find or create entity with id {0}.",
                        operation.EntityInfo.EntityID));
                }

                // For delete operation, delete entity and continue.
                if (operation.State == ObjectState.Deleted)
                {
                    entity.MarkAsDeleted();
                    continue;
                }

                // Apply property operations
                foreach (var pair in operation.PropertyChanges)
                {
                    entity.SetValue(pair.Key, pair.Value);
                }
            }

            foreach (var operation in helper.Operations.Where(o => o.State != ObjectState.Deleted))
            {
                var entity = helper[operation];
                if (entity == null)
                {
                    throw new InvalidOperationException(string.Format("Could not find or create entity with id {0}.",
                        operation.EntityInfo.EntityID));
                }

                // Remove navigation property
                foreach (var pair in operation.NavigationDeletes)
                {
                    entity.SetValue(pair.Key, null);
                }

                // Add navigation property
                foreach (var pair in operation.NavigationAdds)
                {
                    entity.SetValue(pair.Key, helper[pair.Value]);
                }

                // Remove navigation collection
                foreach (var pair in operation.NavigationCollectionDeletes)
                {
                    var collection = (IList)entity.GetValue(pair.Key);
                    foreach (var subOperation in pair.Value)
                    {
                        collection.Remove(helper[subOperation]);
                    }
                }

                // Add navigation collection
                foreach (var pair in operation.NavigationCollectionAdds)
                {
                    var collection = (IList)entity.GetValue(pair.Key);
                    foreach (var subOperation in pair.Value)
                    {
                        collection.Add(helper[subOperation]);
                    }
                }
            }

            var root = helper[tracker];
            if (applyChanges)
            {
                // Accept operation changes to context.
                context.ApplyChanges(tracker.EntityInfo.EntitySetName, root);
            }
            return root;
        }

        #endregion

        private class OperationHelper
        {
            #region Constants and Fields

            private readonly ObjectContext _context;

            private readonly IDictionary<ObjectOperationTracker, IObjectWithOperationTracker> _operations;

            #endregion

            #region Constructors and Destructors

            public OperationHelper(ObjectContext context, ObjectOperationTracker tracker)
            {
                _operations = new Dictionary<ObjectOperationTracker, IObjectWithOperationTracker>();
                _context = context;

                AddAllOperations(tracker);
            }

            #endregion

            #region Properties

            public IEnumerable<ObjectOperationTracker> Operations
            {
                get { return _operations.Keys; }
            }

            #endregion

            #region Indexers

            public IObjectWithOperationTracker this[ObjectOperationTracker key]
            {
                get { return _operations[key]; }
            }

            #endregion

            #region Methods

            private void AddAllOperations(ObjectOperationTracker tracker)
            {
                AddOperation(tracker);
                foreach (var operation in tracker.NavigationDeletes.Where(o => !Contains(o.Value)))
                {
                    AddAllOperations(operation.Value);
                }

                foreach (var operation in tracker.NavigationAdds.Where(o => !Contains(o.Value)))
                {
                    AddAllOperations(operation.Value);
                }

                foreach (var operation in
                    tracker.NavigationCollectionAdds.SelectMany(o => o.Value).Where(o => !Contains(o)))
                {
                    AddAllOperations(operation);
                }

                foreach (var operation in
                    tracker.NavigationCollectionDeletes.SelectMany(o => o.Value).Where(o => !Contains(o)))
                {
                    AddAllOperations(operation);
                }
            }

            private void AddOperation(ObjectOperationTracker tracker)
            {
                if (!_operations.ContainsKey(tracker))
                {
                    _operations.Add(tracker, GetEntity(tracker));
                }
            }

            private bool Contains(ObjectOperationTracker tracker)
            {
                return _operations.ContainsKey(tracker);
            }

            private IObjectWithOperationTracker GetEntity(ObjectOperationTracker tracker)
            {
                IObjectWithOperationTracker entity;
                // If the state is added, create a new instance using the Activator.
                if (tracker.State == ObjectState.Added)
                {
                    entity = (IObjectWithOperationTracker)Activator.CreateInstance(tracker.EntityInfo.EntityType);
                }
                else
                {
                    var entityKey = new EntityKey(tracker.EntityInfo.EntitySetName,
                        tracker.EntityInfo.EntityKeys.Select(pair => new EntityKeyMember(pair.Key, pair.Value)));
                    try
                    {
                        // If at first no entities were found, try to load the metatdata and retry.
                        entity = (IObjectWithOperationTracker)_context.GetObjectByKey(entityKey);
                    }
                    catch (InvalidOperationException)
                    {
                        _context.MetadataWorkspace.LoadFromAssembly(tracker.EntityInfo.EntityType.Assembly);
                        entity = (IObjectWithOperationTracker)_context.GetObjectByKey(entityKey);
                    }
                }
                if (entity != null)
                {
                    // Retain the entity info.
                    entity.OperationTracker.EntityInfo = tracker.EntityInfo;
                }
                return entity;
            }

            #endregion
        }
    }
}